/**
 * Copyright 2013 The FROG Brigade
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.frogbrigade.bufonidae.io;

import java.io.IOException;
import java.io.Writer;

/**
 * 出力ストリームをCSV（Character Separated Values）として書き進めるライターです。
 * <p>
 * CSVは{@link net.frogbrigade.bufonidae.io.CsvReader CsvReader}で解析できる形式で出力します。
 * 
 * @author nobuyuki sekimori
 * @see net.frogbrigade.bufonidae.io.CsvReader CsvReader
 * @since 1.0.0
 * @version 1.0.0
 */
public class CsvWriter
        extends Writer
{
    private static final int CR                = 0x0d;
    private static final int LF                = 0x0a;
    private static final int DEFAULT_DELIMITER = 0x2c;
    private static final int DEFAULT_QUOTE     = 0x22;
    private static final int END_OF_STREAM     = -1;
    private static final int END_OF_RECORD     = -2;
    private static final int END_OF_FIELD      = -3;
    private static final int TOP_OF_STREAM     = -3;
    private static final int TOP_OF_RECORD     = -2;
    private static final int TOP_OF_FIELD      = -1;
    private static final int IN_FIELD          = 0;

    private Writer           out               = null;
    private int              pos               = CsvWriter.TOP_OF_STREAM;
    private int              delimiter         = CsvWriter.DEFAULT_DELIMITER;
    private int              quote             = CsvWriter.DEFAULT_QUOTE;
    private String           lineSeparator     = System.lineSeparator();

    /**
     * CSVとして書き進める出力ストリームを含むライターを指定してオブジェクトを生成します。
     * <p>
     * 区切文字と囲み文字は、それぞれディフォルトのカンマ（0x2c）とダブルクォーテーション（0x22）を使用します。
     * 
     * @param out
     *            CSVとして書き進めるライターです。
     */
    public CsvWriter(Writer out)
    {
        this(out, (char) CsvWriter.DEFAULT_DELIMITER, (char) CsvWriter.DEFAULT_QUOTE);
    }

    /**
     * CSVとして書き進める出力ストリームを含むライター、区切文字と囲み文字を指定してオブジェクトを生成します。
     * <p>
     * 区切り文字と囲み文字に同じ文字、改行文字を指定すると、IllegalArgumentExceptionをスローします。
     * 
     * @param out
     *            CSVとして書き進めるライターです。
     * @param delimiter
     *            区切文字です。
     * @param quote
     *            囲み文字です。
     */
    public CsvWriter(Writer out, char delimiter, char quote)
    {
        super(out);

        if (delimiter == quote) { throw new IllegalArgumentException("same character is specified in delimiter and quote."); }
        if (delimiter == CsvWriter.CR || delimiter == CsvWriter.LF) { throw new IllegalArgumentException("CR or LF are specified in delimiter."); }
        if (quote == CsvWriter.CR || quote == CsvWriter.LF) { throw new IllegalArgumentException("CR or LF are specified in quote."); }

        this.out = out;
        this.delimiter = delimiter;
        this.quote = quote;
    }

    /**
     * CSVの区切文字を取得します。
     * 
     * @return CSVの区切文字です。
     */
    public char getDelimiter()
    {
        return (char) this.delimiter;
    }

    /**
     * CSVの囲み文字を取得します。
     * 
     * @return CSVの囲み文字です。
     */
    public char getQuote()
    {
        return (char) this.quote;
    }

    /**
     * 出力ストリームにCSVとして1文字書き進めます。
     * 
     * @param c
     *            書き進める1文字の0〜65535（0x00-0xffff）の範囲の整数表現です。
     *            <p>
     *            但し、出力ストリームの終わりに達する場合に-1、各レコードの終わりに達する場合に-2、各フィールドの終わりに達する場合に-3
     *            を指定すると、 各フィールドの囲み文字や区切り文字、各レコードの切れ目を表す改行文字を書き進めます。
     *            尚、出力ストリームの終わりに達する場合に-1を指定すると、最後の囲み文字を書き進めた後に出力ストリームを閉じます。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void write(int c)
            throws IOException
    {
        switch (this.pos)
        {
            case CsvWriter.TOP_OF_FIELD:
                this.out.write(this.delimiter);
            case CsvWriter.TOP_OF_RECORD:
            case CsvWriter.TOP_OF_STREAM:
                this.out.write(this.quote);
                this.pos = CsvWriter.IN_FIELD;
                break;
        }

        if (c == CsvWriter.END_OF_FIELD || c == CsvWriter.END_OF_RECORD || c == CsvWriter.END_OF_STREAM)
        {
            this.out.write(this.quote);

            switch (c)
            {
                case END_OF_FIELD:
                    this.pos = CsvWriter.TOP_OF_FIELD;
                    break;
                case END_OF_RECORD:
                    this.out.write(this.lineSeparator);
                    this.pos = CsvWriter.TOP_OF_RECORD;
                    break;
                case END_OF_STREAM:
                    this.pos = CsvWriter.TOP_OF_STREAM;
                    this.close();
                    break;
            }
        }
        else
        {
            this.out.write(c);

            if (c == this.quote)
            {
                this.out.write(this.quote);
            }
        }
    }

    /**
     * 出力ストリームにCSVとして指定した配列の長さ分書き進めます。
     * 
     * @param cbuf
     *            書き込む文字を格納する配列です。
     * @exception IOException
     *                入出力エラーが発生した場合。
     */
    @Override
    public void write(char[] cbuf)
            throws IOException
    {
        synchronized (this.lock)
        {
            this.write(cbuf, 0, cbuf.length);
        }
    }

    /**
     * 出力ストリームにCSVとして指定した配列の指定した位置から指定した文字数分書き進めます。
     * 
     * @param cbuf
     *            書き込む文字を格納する配列です。
     * @param off
     *            書き込む文字の格納を開始する配列の位置です。
     * @param len
     *            書き進める文字数です。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void write(char[] cbuf, int off, int len)
            throws IOException
    {
        synchronized (this.lock)
        {
            if (off < 0 || off > cbuf.length || len < 0 || off + len > cbuf.length || off + len < 0) { throw new IndexOutOfBoundsException(); }

            for (int cbufIndex = 0; cbufIndex < len; cbufIndex++)
            {
                this.write(cbuf[off + cbufIndex]);
            }
        }
    }

    /**
     * 出力ストリームにCSVとして指定した文字列を書き進めます。
     * 
     * @param str
     *            書き込む文字列です。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void write(String str)
            throws IOException
    {
        synchronized (this.lock)
        {
            this.write(str, 0, str.length());
        }
    }

    /**
     * 出力ストリームにCSVとして指定した文字列の指定した位置から指定した文字数分書き進めます。
     * 
     * @param str
     *            書き込む文字を格納する文字列です。
     * @param off
     *            書き込む文字の格納を開始する文字列の位置です。
     * @param len
     *            書き進める文字数です。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void write(String str, int off, int len)
            throws IOException
    {
        synchronized (this.lock)
        {
            char[] cbuf = new char[len];
            str.getChars(off, off + len, cbuf, 0);
            this.write(cbuf, 0, len);
        }
    }

    /**
     * 出力ストリームにCSVとして1レコード分のフィールドを書き進めます。
     * 
     * @param record
     *            　CSVの1レコード分のフィールドです。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    public void writeRecord(String[] record)
            throws IOException
    {
        synchronized (this.lock)
        {
            for (int recordIndex = 0; recordIndex < record.length; recordIndex++)
            {
                if (recordIndex > 0)
                {
                    this.write(CsvWriter.END_OF_FIELD);
                }

                this.write(record[recordIndex]);
            }

            this.write(CsvWriter.END_OF_RECORD);
        }
    }

    /**
     * CSVとして書き進める出力ストリームをフラッシュします。
     * 
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void flush()
            throws IOException
    {
        synchronized (this.lock)
        {
            this.out.flush();
        }
    }

    /**
     * CSVとして書き進める出力ストリームを閉じ、関連するすべてのシステムリソースを解放します。
     * <p>
     * 閉じた後にclose以外の入力ストリームを操作するメソッドを呼び出すと、IOExceptionをスローします。
     * 
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    @Override
    public void close()
            throws IOException
    {
        synchronized (this.lock)
        {
            if (this.out != null)
            {
                try
                {
                    this.flush();
                }
                finally
                {
                    this.out.close();
                }
            }

            this.out = null;
            this.pos = CsvWriter.TOP_OF_STREAM;
        }
    }
}
